// Copyright (C) 2023 即时通讯网(52im.net) & Jack Jiang.
// The RainbowChat Project. All rights reserved.
// 
// 【本产品为著作权产品，合法授权后请放心使用，禁止外传！】
// 【本次授权给：<北京小羊驼科技有限公司>，授权编号：<NT231212144350>，代码指纹：<A.702363430.550>，技术对接人微信：<ID: wxid_wbyootvkdcgj22>】
// 【授权寄送：<收件：苏先生、地址：北京市大兴区北京密码西区6号楼1单元301、电话：18613885610、邮箱：bd@huiyuanxiang-inc.com>】
// 
// 【本系列产品在国家版权局的著作权登记信息如下】：
// 1）国家版权局登记名(简称)和权证号：RainbowChat    （证书号：软著登字第1220494号、登记号：2016SR041877）
// 2）国家版权局登记名(简称)和权证号：RainbowChat-Web（证书号：软著登字第3743440号、登记号：2019SR0322683）
// 3）国家版权局登记名(简称)和权证号：RainbowAV      （证书号：软著登字第2262004号、登记号：2017SR676720）
// 4）国家版权局登记名(简称)和权证号：MobileIMSDK-Web（证书号：软著登字第2262073号、登记号：2017SR676789）
// 5）国家版权局登记名(简称)和权证号：MobileIMSDK    （证书号：软著登字第1220581号、登记号：2016SR041964）
// * 著作权所有人：江顺/苏州网际时代信息科技有限公司
// 
// 【违法或违规使用投诉和举报方式】：
// 联系邮件：jack.jiang@52im.net
// 联系微信：hellojackjiang
// 联系QQ号：413980957
// 授权说明：http://www.52im.net/thread-1115-1-1.html
// 官方社区：http://www.52im.net
package com.x52im.rbchat.logic.chat_root.sendfile;

import android.app.Activity;
import android.content.Context;
import android.util.Log;

import com.eva.android.ToolKits;
import com.eva.android.widget.DataLoadingAsyncTask;
import com.eva.android.widget.WidgetUtils;
import com.eva.epc.common.util.CommonUtils;
import com.x52im.rbchat.Const;
import com.x52im.rbchat.MyApplication;
import com.x52im.rbchat.logic.chat_guest.utils.TChatDataHelper;
import com.x52im.rbchat.logic.chat_guest.utils.TMessageHelper;
import com.x52im.rbchat.logic.chat_root.model.MessageExt;
import com.x52im.rbchat.network.http.bigfile.BigFileUploadManager;
import com.x52im.rbchat.logic.chat_root.model.Message;
import com.x52im.rbchat.logic.chat_root.model.FileMeta;
import com.x52im.rbchat.logic.chat_friend.utils.ChatDataHelper;
import com.x52im.rbchat.logic.chat_friend.utils.MessageHelper;
import com.x52im.rbchat.logic.chat_group.utils.GChatDataHelper;
import com.x52im.rbchat.logic.chat_group.utils.GMessageHelper;

import net.x52im.mobileimsdk.server.protocal.Protocal;

import java.io.File;
import java.util.Observable;
import java.util.Observer;

/**
 * 文件消息发送的真正实现类.
 *
 * @author Jack Jiang(http://www.52im.net/space-uid-1.html)
 * @since 4.3
 */
public class SendFileProcessor
{
    private final static String TAG = SendFileProcessor.class.getSimpleName();

    private Context context = null;
    
    /** 将要被发送的文件路径 */
    private String filePath = null;

    /** 用于一对一聊天时表示消息接收者的uid、用于群聊时本字段表示文件接收的目标群id */
    protected String toId = null;

    /**
     * 当前仅用于一对一陌生人聊天时表示消息接收者的昵称.
     *
     * @since 5.1
     */
    protected String toName = null;

    /**
     * 功能应用场景:如用于陌生人聊天、一对一好友聊、世界频道聊等.
     *
     * @see Const#CHAT_TYPE_GUEST$CHAT
     * @see Const#CHAT_TYPE_GUEST$CHAT
     * @see Const#CHAT_TYPE_GROUP$CHAT
     */
    protected String usedFor = "";

    /**
     * 构造方法。
     *
     * @param context 上下文对象
     * @param usedFor 功能应用场景:如用于陌生人聊天、一对一好友聊、世界频道聊等，
     *                参见：{@link Const#CHAT_TYPE_FREIDN$CHAT}、{@link Const#CHAT_TYPE_GUEST$CHAT}、{@link Const#CHAT_TYPE_GROUP$CHAT}
     * @param toId 用于一对一聊天时表示消息接收者的uid、用于群聊时本字段表示文件接收的目标群id
     * @param toName 本字段当前仅用于陌生人聊天时，其它聊天类型时请传null即可
     * @param filePath 要发送的文件绝对路径（此路径在AndriodQ及以上版本时应为私有目录路径）
     */
    public SendFileProcessor(Context context, String usedFor, String toId, String toName, String filePath)
    {
        this.context = context;
        this.usedFor = usedFor;
        this.toId = toId;
        this.toName = toName;
        this.filePath = filePath;
    }

    /**
     * 文件发送.
     */
    public void doSend()
    {
        boolean beforeSendOK = beforeSend();
        if(beforeSendOK) {
            final File f = new File(filePath);

            // 计算MD5码是个耗时的过程，一个30M左右的文件，在千元Android机上可能需要2秒左右
            // ，200M左右的大文件耗时可能超过1分钟，所以使用异步线程来执行可以提升用户体验
            new DataLoadingAsyncTask<String, Integer, String>(context, "文件准备中，请稍候..") {
                @Override
                protected String doInBackground(String... params) {
                    try {
                        // 计算MD5码（本方法可保证在计算大至数G的文件时，也不会发生OOM）
                        return ToolKits.getFileMD5(filePath);
                    } catch(Exception e) {
                        Log.w(TAG, e);
                        return null;
                    }
                }

                protected void onPostExecuteImpl(Object result) {
                    if(result != null && result instanceof String) {
                        final String fileMD5 = (String)result;
                        // 开始执行文件发送的完整处理逻辑
                        SendFileProcessor.doSendLogic(context, false, toId, toName, usedFor, filePath, fileMD5);
                    } else {
                        WidgetUtils.showToast(context, "文件的MD5码计算失败，发送已被取消！", WidgetUtils.ToastType.WARN);
                    }
                }
            }.execute();
        }
    }

    /**
     * 发送前的检查过程.
     *
     * @return true表示检查通过，否则不合法的文件不应被发送
     */
    public boolean beforeSend()
    {
        boolean sucess = false;

        try
        {
            File f = new File(filePath);

            // 文件是否存的检查
            if(filePath == null || !f.exists())
            {
                Log.w(TAG, "【SendFile】要发送的文件"+filePath+"不存在，本地发送没有继续！");
                WidgetUtils.showToast(context, "文件不存在，发送已被取消！", WidgetUtils.ToastType.WARN);
                return false;
            }

            // 是否文件夹检查
            if(f.isDirectory())
            {
                Log.w(TAG, "【SendFile】要发送的"+filePath+"不是文件，本地发送没有继续！");
                WidgetUtils.showToast(context, "不是文件，发送已被取消！", WidgetUtils.ToastType.WARN);
                return false;
            }

            // 允许发送的最大文件大小的检查
            if(f.length() > Const.SEND_FILE_DATA_MAX_LENGTH || f.length() <= 0)
            {
                Log.w(TAG, "【SendFile】要发送的文件"+filePath+"大小非法，(MAX="+ Const.SEND_FILE_DATA_MAX_LENGTH
                        +"字节)，本地发送没有继续！");
                WidgetUtils.showWithDialog(context, "文件超限提示", "文件过大，当前允许最大发送"
                        +CommonUtils.getConvenientFileSize(Const.SEND_FILE_DATA_MAX_LENGTH, 0)
                        +"的文件，本次发送已取消！");
                return false;
            }

            sucess = true;
        }
        catch (Exception e)
        {
            Log.e(TAG, "【SendFile】文件发送前的检查过程中出错了！", e);
            WidgetUtils.showToast(context, "文件发送失败，请重试！", WidgetUtils.ToastType.INFO);
        }

        return sucess;
    }

    /**
     * 真正的文件发送完整逻辑的方法。
     *
     * @param context Context
     * @param forForward true表示用于消息转发功能时，否则表示是正常的文件发送
     * @param toId 接收者（如果是一对一聊天则本参数是对方的uid，如果是世界频道或群聊则本参数为gid）
     * @param toName 接收者名字（本字段当前仅用于陌生人聊天时，其它聊天类型时请传null即可）
     * @param usedFor 用途
     * @param filePath 文件完整路径
     * @param fileMD5 文件md5码
     */
    public static void doSendLogic(final Context context, boolean forForward, final String toId, final String toName
            , final String usedFor, String filePath, final String fileMD5) {
        doSendLogic(context, forForward, toId, toName, usedFor, filePath, fileMD5, 0, null);
    }

    /**
     * 真正的文件发送完整逻辑的方法。
     *
     * @param context Context
     * @param forForward true表示用于消息转发功能时，否则表示是正常的文件发送
     * @param toId 接收者（如果是一对一聊天则本参数是对方的uid，如果是世界频道或群聊则本参数为gid）
     * @param toName 接收者名字（本字段当前仅用于陌生人聊天时，其它聊天类型时请传null即可）
     * @param usedFor 用途
     * @param filePath 文件完整路径
     * @param fileMD5 文件md5码
     * @param fileLenForForward 文件大小（单位：字节），此字段目前仅用于消息转发时（其它情况请传0），因为转发收到的文件
     *                   消息时，因此时文件可能尚未下载，如果直接通过从本地文件信息中读大小那就不可能了
     * @param sucessObsExtra 消息发送完成后，额外要做的事
     */
    public static void doSendLogic(final Context context, boolean forForward, final String toId, final String toName
            , final String usedFor, String filePath, final String fileMD5, long fileLenForForward, final Observer sucessObsExtra)
    {
        final File f = new File(filePath);

        final String fileName = f.getName();

        // 当转发收到的文件消息时，因此时文件可能尚未下载，如果直接通过从本地文件信息中读大小那就不可能了
        long fileLengthTemp = f.length();
        // 只能用转发的参数中文件大小值
        if(fileLengthTemp == 0) {
            fileLengthTemp = fileLenForForward;
        }

        final long fileLength = fileLengthTemp;

        if(!forForward) {
            // 检查一下此md5码的文件是否正在上传中，如果真在上传中就不允许本次再发了（强行再次上传会打乱
            // 上个上传的断点算法和逻辑，而且从实际意义来说同一个文件完全没必要重复上传，等上一次发的传完再发不迟！）
            BigFileUploadManager um = MyApplication.getInstance(context).getIMClientManager().getBigFileUploadManager();
            if (um != null) {
                if (um.isUploading(fileMD5)) {
                    Log.d(TAG, "【大文件上传】要上传的大文件：" + filePath + "， 已存在相同的上传任务，本次任务没有继续！");
                    WidgetUtils.showToast(context, "文件" + fileName + "已经在发送中，无需重复发送！"
                            , WidgetUtils.ToastType.INFO);
                    return;
                }
            }
        }

        Message cme = null;
        // 先生成指纹码
        final String fingerPring = Protocal.genFingerPrint();
        FileMeta fileMeta = new FileMeta(fileName, fileMD5, fileLength, filePath);
        // 用于正式聊天的文件发送
        if (Const.CHAT_TYPE_FREIDN$CHAT.equals(usedFor)) {
            cme = ChatDataHelper.addChatMessageData_outgoing(context, toId, MessageExt.createChatMsgEntity_OUTGO_FILE(fileMeta, fingerPring));
        }
        // 用于陌生人聊天的文件发送
        else if(Const.CHAT_TYPE_GUEST$CHAT.equals(usedFor)) {
            cme = TChatDataHelper.addChatMessageData_outgoing(context, toId, MessageExt.createChatMsgEntity_OUTGO_FILE(fileMeta, fingerPring));
        }
        // 用于群聊时
        else if (Const.CHAT_TYPE_GROUP$CHAT.equals(usedFor)) {
            cme = GChatDataHelper.addChatMessageData_outgoing(context, toId, MessageExt.createChatMsgEntity_OUTGO_FILE(fileMeta, fingerPring));
        }

        // 观察者：用于文件上传完成时通知本方法的调用者来做余下的事（把这个观察者当回调来理解就好了）
        Observer observerForFileUploadProcessOK = (observable, data) -> {
            final FileMeta fm = new FileMeta(fileName, fileMD5, fileLength
                    // 注意：本数据是要发送给对方的，所以此路径是对方收到后的保存绝对路径，且应该是由对方收到消息后来决定（因为手机型号不同可能文件目录的规则不相同，比如老的android跟android 4.4以上就不同）
                    // , ReceivedFileHelper.getReceivedFileSavedDirHasSlash(context) + fileName
                    , null);
            // 用于正式聊天的文件发送
            if (Const.CHAT_TYPE_FREIDN$CHAT.equals(usedFor)) {
                // 并发送一条文件消息给好友（文件上传逻辑已在“SendFileHelper.processBigFileUpload(..)”处理完）
                MessageHelper.sendFileMessageAsync((Activity) context, toId, fm, fingerPring, sucessObsExtra);
            }
            // 用于陌生人聊天的文件发送
            else if (Const.CHAT_TYPE_GUEST$CHAT.equals(usedFor)) {
                // 并发送一条文件消息给陌生人（文件上传逻辑已在“SendFileHelper.processBigFileUpload(..)”处理完）
                TMessageHelper.sendFileMessageAsync((Activity) context, toId, toName, fm, fingerPring, sucessObsExtra);
            }
            // 用于群聊时
            else if (Const.CHAT_TYPE_GROUP$CHAT.equals(usedFor)) {
                // 并发送一条文件消息给群友（文件上传逻辑已在“SendFileHelper.processBigFileUpload(..)”处理完）
                GMessageHelper.sendFileMessageAsync((Activity) context, toId, fm, fingerPring, sucessObsExtra);
            }
        };

        if(!forForward) {
            // 开始处理真正的文件上传完整逻辑
            SendFileHelper.processBigFileUpload(context, fileName, filePath, fileMD5, cme, observerForFileUploadProcessOK);
        }
        // 如果是用于消息转发功能就不需要走文件的处理逻辑，直接ui显示
        else{

            //** 以下设置没在 sucessObsExtra 观察者里做的原因：是大文件型的上传不同于普通消息，普通消息是等到网络指
            //** 令发出成合后才会将Message对象插入到ui界面等，而大文件型的消息是无论文件有没有处理完成、网络指令有没
            //** 发出，它都会先将Message对象插入到ui界面（先显示），然后再通过消息上的各种状态是告之用户此条文件型消息的状态是到什么程度了。
            // 转发的消息不需要处理文件上传，直接默认就认为文件已经上传好了（因为功能就只允许转发成功发出的文件消息）
            cme.setSendStatusSecondary(Message.SendStatusSecondary.processOk);
            // 设置标识，以便消息发送失败时，用户点击重传时进行特殊的逻辑处理
            cme.setForwardOutgoing(true);

            // 直接通知观察者刷新ui
            observerForFileUploadProcessOK.update(null, null);
        }
    }
}
